﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using Silvermoon.OpenGL.Native;
using System.Drawing.Imaging;

namespace Silvermoon.OpenGL
{
    public partial class FontSprite
    {
        /// <summary>
        /// this is ascii for "SMFONT":
        /// </summary>
        private static readonly byte[] magicHeader = new byte[] { 0x53, 0x4d, 0x46, 0x4f, 0x4e, 0x54 };

        private static readonly int currentVersion = 1;

        /// <summary>
        /// Creates a new <see cref="FontSprite"/> from a stream which contains a data structure for the font.
        /// See also <seealso cref="FontSprite.WriteToStream"./>
        /// </summary>
        /// <param name="stream">Stream to read from.</param>
        /// <returns>New FontSprite instance.</returns>
        public static FontSprite FromStream(Stream stream)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            return new FontSprite(stream);
        }

        /// <summary>
        /// Creates a new <see cref="FontSprite"/> from a file.
        /// </summary>
        /// <param name="path">Path and name of the file.</param>
        /// <param name="kind">Specifies whether the path is relative to the code base of the executing assembly, or absolute.</param>
        /// <returns>New FontSprite instance.</returns>
        public static FontSprite FromFile(string path, UriKind kind)
        {
            path = GetPhysicalFileName(path, kind);
            using (Stream stream = File.OpenRead(path))
            {
                return FromStream(stream);
            }
        }

        protected FontSprite(Stream stream)
            : base()
        {
            refCount = 1;
            LoadFromStream(stream);
        }

        /// <summary>
        /// Creates a new <see cref="FontSprite"/> from a file if the file exists.
        /// </summary>
        /// <param name="path">Path and name of the file.</param>
        /// <param name="kind">Specifies whether the path is relative to the code base of the executing assembly, or absolute.</param>
        /// <returns>New FontSprite instance if the file exists, otherwise null.</returns>
        public static FontSprite FromFileOrNull(string path, UriKind kind)
        {
            path = GetPhysicalFileName(path, kind);
            if (!File.Exists(path)) return null;
            using (Stream stream = File.OpenRead(path))
            {
                return FromStream(stream);
            }
        }

        private static string GetPhysicalFileName(string fileName, UriKind kind)
        {
            switch (kind)
            {
                case UriKind.RelativeOrAbsolute: throw new NotSupportedException("UriKind.RelativeOrAbsolute.");
                case UriKind.Absolute: return fileName;
                case UriKind.Relative: return Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase), fileName);
                default: throw new NotSupportedException();
            }
        }


        /// <summary>
        /// Writes all required data to a file which can be loaded with FontSprite.FromStream.
        /// </summary>
        /// <param name="path">Path and name of the file.</param>
        /// <param name="kind">Specifies whether the path is relative to the code base of the executing assembly, or absolute.</param>
        public void WriteToFile(string path, UriKind kind)
        {
            path = GetPhysicalFileName(path, kind);
            using (Stream stream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                WriteToStream(stream);
            }
        }

        /// <summary>
        /// Writes all required data to a stream which can be loaded with FontSprite.FromStream.
        /// </summary>
        /// <param name="stream">Stream to write.</param>
        public void WriteToStream(Stream stream)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            using (DeflateStream ds = new DeflateStream(stream, CompressionMode.Compress))
            {
                BinaryWriter writer = new BinaryWriter(ds);
                writer.Write(magicHeader);
                writer.Write(currentVersion);

                writer.Write((short)this.MappedCharacters);
                writer.Write((short)this.CharHeight);
                writer.Write((short)this.MaxCharWidth);
                writer.Write((short)this.TrailingSpace);
                writer.Write((short)this.LeadingSpace);
                writer.Write((short)this.TextureSize.Width);
                writer.Write((short)this.TextureSize.Height);
                writer.Write((int)(this.Style));
                writer.Write(this.Name);
                writer.Write(this.Size);
                writer.Write(this.IsShadowed);

                byte[] rawImage = null;
                using (Font font = new Font(this.Name, this.Size, this.Style))
                {
                    rawImage = this.GetPixelData(font);
                }
                int numBytes = rawImage.Length;
                writer.Write(numBytes);

                foreach (var pair in this.Characters)
                {
                    CharData data = pair.Value;
                    writer.Write(pair.Key);
                    writer.Write(data.Width);
                    writer.Write(data.X);
                    writer.Write(data.Y);
                }

                writer.Write(rawImage);
            }
        }

        private void LoadFromStream(Stream stream)
        {
            if (stream == null) throw new ArgumentNullException("stream");

            texture = new Texture();
            try
            {
                using (DeflateStream ds = new DeflateStream(stream, CompressionMode.Decompress))
                {
                    BinaryReader reader = new BinaryReader(ds);
                    CheckFile(reader);
                    int characterCount = reader.ReadInt16();
                    this.CharHeight = reader.ReadInt16();
                    this.MaxCharWidth = reader.ReadInt16();
                    this.TrailingSpace = reader.ReadInt16();
                    this.LeadingSpace = reader.ReadInt16();
                    Size size = new Size();
                    size.Width = reader.ReadInt16();
                    size.Height = reader.ReadInt16();
                    this.TextureSize = size;

                    this.Style = (FontStyle)reader.ReadInt32();
                    this.Name = reader.ReadString();
                    this.Size = reader.ReadSingle();
                    this.IsShadowed = reader.ReadBoolean();

                    int numBytes = reader.ReadInt32();

                    this.Characters = new Dictionary<char, CharData>();
                    for (int i = 0; i < characterCount; i++)
                    {
                        CharData data = new CharData();
                        char c = reader.ReadChar();
                        data.Width = reader.ReadInt16();
                        data.X = reader.ReadInt16();
                        data.Y = reader.ReadInt16();
                        Characters[c] = data;
                    }
                    byte[] rawImage = reader.ReadBytes(numBytes);
                    TextureFormat format = GetTextureFormat(this.IsShadowed);
                    this.texture.FromByteArray(rawImage, format, size);
                }
            }
            catch (Exception e)
            {
                throw new FontReaderException("Reading error", e);
            }
        }

        private byte[] GetPixelData(Font font)
        {
            int bmWidth = TextureSize.Width;
            int bmHeight = TextureSize.Height;

            int charHeight = CharHeight;
            int trail = (TrailingSpace + LeadingSpace) / 2;
            bool shadowed = IsShadowed;
            SolidBrush shadowBrush = shadowed ? new SolidBrush(Color.Black) : null;
            Color transparentColor = shadowed ? Color.Fuchsia : Color.Black;
            TextureFormat format = GetTextureFormat(shadowed);
            PixelFormat pFormat = TextureUtil.GetPixelFormatFromTextureFormat(format);

            using (Bitmap bitmap = new Bitmap(bmWidth, bmHeight, pFormat))
            {
                using (Graphics g = Graphics.FromImage(bitmap))
                {
                    g.Clear(transparentColor);
                    using (SolidBrush brush = new SolidBrush(Color.White))
                    {
                        int x = 0;
                        int y = 0;
                        foreach (var pair in Characters)
                        {
                            char c = pair.Key;
                            CharData cd = pair.Value;
                            int charWidth = cd.Width;
                            if (charWidth > 0)
                            {
                                if ((x + charWidth) >= bmWidth)
                                {
                                    x = 0;
                                    y += charHeight;
                                    if ((y + charHeight) >= bmHeight)
                                    {
                                        y = bmHeight - charHeight;
                                    }
                                }
                                string s = c.ToString();
                                if (shadowed) g.DrawString(s, font, shadowBrush, x + 1.5f, y + 1.5f);
                                g.DrawString(s, font, brush, x, y);
                            }
                            x += charWidth + trail;
                        }
                    }
                }
                return texture.GetBytesFromBitmap(bitmap, format, transparentColor);
            }
        }

        private static TextureFormat GetTextureFormat(bool shadowed)
        {
            TextureFormat format = shadowed ? TextureFormat.LuminanceAlpha : TextureFormat.Alpha;
            return format;
        }

        private static void CheckFile(BinaryReader reader)
        {
            byte[] headerInfo = reader.ReadBytes(magicHeader.Length);
            if (!IsCorrectHeader(headerInfo)) throw new FontReaderException("Invalid file structure.");
            int version = reader.ReadInt32();
            if (version != currentVersion) throw new FontReaderException("File version is not supported.");
        }

        private static bool IsCorrectHeader(byte[] headerInfo)
        {
            if (headerInfo.Length != magicHeader.Length) return false;
            for (int i = 0; i < magicHeader.Length; i++)
            {
                if (headerInfo[i] != magicHeader[i]) return false;
            }
            return true;
        }
    }
}
